package ubc.cs.cpsc210.sustainabilityapp.routing;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;

import android.location.Location;
import android.util.Log;

/**
 * Wrapper around a service which calculates routes between geographic locations.  This class may 
 * be called concurrently from multiple threads -- it is thread-safe.
 * 
 * Currently, this class wraps the www.yournavigation.org API 
 * (<a href="http://wiki.openstreetmap.org/wiki/YOURS#Routing_API">http://wiki.openstreetmap.org/wiki/YOURS#Routing_API</a>).  
 */
public class RoutingService {
	private final static String LOG_TAG = "RoutingService";

	/**
	 * Caches routes retrieved by their endpoints.  Access to this map must be synchronized on the
	 * map.
	 */
	private final Map<RouteEndpoints, RouteInfo> routeCache = new HashMap<RouteEndpoints, RouteInfo>();

	/** 
	 * Client for making HTTP requests to the API of the service.
	 */
	private HttpClient client;

	public RoutingService() {
		// Create an HttpClient with the ThreadSafeClientConnManager.
		// This connection manager must be used if more than one thread will
		// be accessing the HttpClient.
		HttpParams params = new BasicHttpParams();
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

		SchemeRegistry schemeRegistry = new SchemeRegistry();
		schemeRegistry.register(
				new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));

		ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
		client = new DefaultHttpClient(cm, params);
	}

	public void shutdown() {
		if (client != null) {
			client.getConnectionManager().shutdown();
			client = null;
		}
	}

	/**
	 * Calculate route for given start point and end point.  An internet connection must be available.
	 * See {@link getRouteFromService} for further information on route generation.
	 * 
	 * @param start The start point of the route.
	 * @param end The end point of the route.
	 * @param useCache Indicates whether the service should return a cached route, if one exists.  
	 *                 If this flag is set to true, and a cached route is not available, then the new 
	 *                 route obtained from the server will be cached.
	 * @return Information on the route calculated, including the waypoints.
	 * @throws IOException If an error occurs while retrieving the route from the server.
	 */
	public RouteInfo getRoute(Location start, Location end, boolean useCache) throws IOException {
		RouteEndpoints endpoints = new RouteEndpoints(start, end);
		RouteInfo result = null;
		if (useCache) {
			result = getCachedRoute(endpoints);
			if (result != null) {
				return result;
			}
		}

		result = getRouteFromService(endpoints);
		if (useCache && result != null) {
			addRouteToCache(endpoints, result);
		}
		return result;
	}

	/**
	 * Calculate route for given endpoints.  Currently, we use the www.yournavigation.org API
	 * (<a href="http://wiki.openstreetmap.org/wiki/YOURS#Routing_API">http://wiki.openstreetmap.org/wiki/YOURS#Routing_API</a>), 
	 * with result format set to geojson, vehicle set to foot and route type set to shortest (rather than fastest).  Using route 
	 * type of fastest can result in different routes between the same two points, depending on the 
	 * direction traveled.
	 * 
	 * Subclasses can override this method to connect to alternate routing services.
	 * 
	 * @param endpoints Endpoints of the route.
	 * @return Information on the route calculated, including waypoints.
	 * @throws IOException If an error occurs while retrieving the route from the server.
	 */
	protected RouteInfo getRouteFromService(RouteEndpoints endpoints) throws IOException {
		RouteInfo result = null;

		synchronized(client) {
			try {
				StringBuilder uriBuilder = new StringBuilder("//www.yournavigation.org/api/1.0/gosmore.php");
				uriBuilder.append("?format=geojson&v=foot&fast=0");
				uriBuilder.append("&flat=").append(endpoints.getStart().getLatitude());
				uriBuilder.append("&flon=").append(endpoints.getStart().getLongitude());
				uriBuilder.append("&tlat=").append(endpoints.getEnd().getLatitude());
				uriBuilder.append("&tlon=").append(endpoints.getEnd().getLongitude());

				URI uri = new URI("http", uriBuilder.toString(), null);

				// Let them know who we are.
				HttpGet getRequest = new HttpGet(uri);
				getRequest.addHeader("X-Yours-client", "UBC CPSC 210");

				ResponseHandler<String> responseHandler = new BasicResponseHandler();
				String responseBody = client.execute(getRequest, responseHandler);

				JSONObject obj = (JSONObject)new JSONTokener(responseBody).nextValue();

				List<Location> waypoints = new ArrayList<Location>();

				JSONArray coords = obj.getJSONArray("coordinates");

				for (int i = 0; i < coords.length(); i++) {
					JSONArray element = coords.getJSONArray(i);
					Location wp = new Location("");
					wp.setLatitude(element.getDouble(1));
					wp.setLongitude(element.getDouble(0));
					waypoints.add(wp);
				}
				result = new RouteInfo(waypoints);
			} catch (IOException e) { 
				throw e;
			} catch (Exception e) {
				// Log it, since we will lose some exception details in the conversion to IOException.
				Log.e(LOG_TAG, "Unexpected exception retrieving route from www.yournavigation.org", e);

				// Convert other exception types to IOException so clients do not have to worry about the different possibilities.
				throw new IOException("Unexpected exception retrieving route from www.yournavigation.org: " + e);
			}
		}
		return result;
	}

	private RouteInfo getCachedRoute(RouteEndpoints endpoints) {
		synchronized (routeCache) {
			return routeCache.get(endpoints);
		}
	}

	private void addRouteToCache(RouteEndpoints endpoints, RouteInfo routeInfo) {
		synchronized (routeCache) {
			routeCache.put(endpoints, routeInfo);
		}
	}
}
